﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace com.citizensoft.MessageExchange
{
    public delegate void ClientMessageReceivedHandler(IPEndPoint from,Stream stream, Message message);
    public delegate void ClientDisconnectedHandler(TcpClient client);

    public class Server : IDisposable
    {
        public int Port { get; private set; }
        public string IPAddress { get; private set; }
        public event ClientMessageReceivedHandler ClientMessageReceived = null;
        public event ClientDisconnectedHandler ClientDisconnected = null;

        public X509Certificate2 ServerCert { get; private set; }

        private TcpListener _listener = null;
        private bool _clientCertRequired = false;

        public Server(int port)
            : this("localhost", port)
        {
        }

        public Server(String ipAddress, int port, string serverCertPath = null, string serverCertPassword = null, bool clientCertRequired = false)
        {
            this.IPAddress = ipAddress;
            this.Port = port;

            if (ipAddress == "localhost")
            {
                _listener = new TcpListener(System.Net.IPAddress.Any, port);
            }
            else
            {
                _listener = new TcpListener(System.Net.IPAddress.Parse(ipAddress), port);
            }

            if (!string.IsNullOrWhiteSpace(serverCertPath) && File.Exists(serverCertPath))
            {
                try
                {
                    if (string.IsNullOrWhiteSpace(serverCertPassword))
                    {
                        ServerCert = new X509Certificate2(serverCertPath);
                    }
                    else
                    {
                        ServerCert = new X509Certificate2(serverCertPath, serverCertPassword);
                    }
                }
                catch { }
            }
        }

        public void Start()
        {
            _listener.Start();
            _listener.BeginAcceptTcpClient(new AsyncCallback(AcceptTcpClient), null);
        }

        public void Stop()
        {
            _listener.Stop();
        }

        private void AcceptTcpClient(IAsyncResult obj)
        {
            TcpClient client = null;
            try
            {
                client = _listener.EndAcceptTcpClient(obj);
            }
            finally
            {
                _listener.BeginAcceptTcpClient(new AsyncCallback(AcceptTcpClient), null);
            }

            if (client != null)
            {
                client.ReceiveBufferSize = 4096;
                client.SendBufferSize = 4096;
                if (ServerCert != null)
                {
                    SslStream stream = new SslStream(client.GetStream(),false);
                    Context context = new Context(client, stream);
                    stream.BeginAuthenticateAsServer(ServerCert, _clientCertRequired, System.Security.Authentication.SslProtocols.Tls, false, new AsyncCallback(AuthenticateClient), context);
                }
                else
                {
                    Context context = new Context(client, client.GetStream());
                    context.Stream.BeginRead(context.Buffer, 0, context.Buffer.Length, new AsyncCallback(MessageRead), context);
                }
            }
        }

        private void AuthenticateClient(IAsyncResult obj)
        {
            Context context = obj.AsyncState as Context;
            ((SslStream)context.Stream).EndAuthenticateAsServer(obj);
            if (((SslStream)context.Stream).IsAuthenticated)
            {
                context.Stream.BeginRead(context.Buffer, 0, context.Buffer.Length, new AsyncCallback(MessageRead), context);
            }
            else
            {
                context.Client.Close();
            }
        }

        private void MessageRead(IAsyncResult obj)
        {
            Context context = obj.AsyncState as Context;
            Message message = null;
            try
            {
                int count = context.Stream.EndRead(obj);
                context.AcceptBuffer(count);
                message = context.ExtractMessage();
                context.Stream.BeginRead(context.Buffer, 0, context.Buffer.Length, new AsyncCallback(MessageRead), context);
            }
            catch (IOException)
            {
                if (ClientDisconnected != null)
                {
                    ClientDisconnected(context.Client);
                }
            }
            finally
            {
                if (message != null && ClientMessageReceived != null)
                {
                    ClientMessageReceived((IPEndPoint)context.Client.Client.RemoteEndPoint,context.Stream, message);
                }
            }
        }

        public void Dispose()
        {
            if (_listener != null)
            {
                _listener.Stop();
                _listener = null;
            }
        }
    }
}